package org.hackystat.devcathlon.engine.event.impl;

import javax.xml.datatype.XMLGregorianCalendar;
import org.hackystat.dailyprojectdata.client.DailyProjectDataClient;
import org.hackystat.dailyprojectdata.resource.coverage.jaxb.CoverageDailyProjectData;
import org.hackystat.devcathlon.engine.event.Event;
import org.hackystat.devcathlon.engine.event.EventException;
import org.hackystat.devcathlon.engine.event.EventScore;
import org.hackystat.devcathlon.engine.match.Match;
import org.hackystat.devcathlon.engine.team.Team;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.resource.projects.jaxb.Project;
import org.hackystat.sensorbase.resource.sensordata.jaxb.Property;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
import org.hackystat.utilities.logger.HackystatLogger;
import org.hackystat.utilities.tstamp.Tstamp;

/**
 * Awards points if the team has increased coverage after committing code, deducts points if
 * coverage has decreased after committing code.
 * <p>
 * Requires the following properties in the configuration.xml file for this event:
 * <ul>
 * <li>WakeupInterval: How frequently this event is invoked, and how far back it looks for data.
 * <li>BaseScoreWeight: An integer multiplied by the base score to get the base points.
 * <li>UpperThreshold: An integer indicating the threshold for "high" coverage, such as 90.
 * <li>TypeOfCoverage: A string indicating the kind of coverage to retrieve, such as "Line".
 * <li>FirstTimeOverThresholdWeight: An integer multiplied by the FirstTimeOverThreshold bonus score
 * to determine the bonus points.
 * </ul>
 * <p>
 * Specification:
 * <ul>
 * <li>Checks all Build sensor data since the last wakeup interval for the first build sensor data
 * instance with a Type property equal to "continuous.integration". Records the timestamp associated
 * with that sensor data.
 * <li>Retrieves the Coverage DPD associated with this project. Checks the timestamp associated with
 * it to see if this data has been generated after the CI build event.
 * <li>If Coverage has been generated after the build, then award base and bonus points, otherwise
 * do nothing.
 * <li>Base Score: 0 if over UpperThreshold or if no previous coverage data exists. Otherwise base
 * score is between -10 and +10 depending upon percentage increase or decrease since last coverage
 * measurement.
 * <li>Bonus Score: Award Bonus Score of type "FirstTimeOverThreshold" if this is the first recorded
 * time that this team is over UpperThreshold.
 * </ul>
 * 
 * @author Philip Johnson
 * 
 */
public class KeepTheCoverageHigh extends Event {

  /** Support serialization. */
  private static final long serialVersionUID = 1L;
  /** The last stored value for coverage. */
  private int lastCoverage = 0;
  /** Whether or not we have a prior coverage value for this team and match. */
  boolean hasLastCoverage = false;
  /** Whether or not this team has received the FirstTimeOverThreshold bonus point. */
  boolean hasFirstTimeBonus = false;
  /** The unique name for this event. */
  public static final String eventName = "Keep The Coverage High";
  /** The unique description for this event. */
  public static final String description = "Reward (or penalize) developers who commit code that "
      + "improves (or degrades) the quality of the test suite. ";
  /** The property key for the upper threshold. */
  public static final String upperThresholdKey = "UpperThreshold";
  /** The actual upper threshold. */
  private int upperThreshold;
  /** The property key for the type of coverage. */
  public static final String typeOfCoverageKey = "TypeOfCoverage";
  /** The actual type of coverage. */
  private String typeOfCoverage;
  /** The property key for the bonus score weight. */
  public static final String firstTimeKey = "FirstTimeOverThreshold";
  /** The actual bonus score weight. */
  private int firstTimeWeight;
  /** The name of the bonus for going over threshold for the first time. */
  public static final String firstTimeBonusName = "FirstTimeBonus";

  /**
   * Creates a new event for Keeping The Coverage High. Ensures all required properties are
   * available.
   * 
   * @throws EventException If the configuration cannot be created.
   */
  public KeepTheCoverageHigh() throws EventException {
    super(eventName, description);
    try {
      upperThreshold = Integer.parseInt(configManager.getProperty(eventName, upperThresholdKey));
      typeOfCoverage = configManager.getProperty(eventName, typeOfCoverageKey);
      firstTimeWeight = Integer.parseInt(configManager.getProperty(eventName, firstTimeKey));
    }
    catch (Exception e) {
      throw new EventException("Error configuring properties", e);
    }
  }

  /**
   * Returns the total number of points for this event. The total is the base score times the base
   * score weight plus the bonus score times the bonus score weight.
   * 
   * @param score The event score.
   * @return The total points for this event given the current weights.
   */
  public int computePoints(EventScore score) {
    return ((score.getBaseScore() * this.baseScoreWeight) + (score
        .getBonusScore(firstTimeBonusName) * this.firstTimeWeight));
  }

  /**
   * Creates an EventScore instance for KeepTheCoverageHigh.
   * 
   * @param match The match of interest.
   * @param team The team of interest.
   * @param timestamp the time at which this event is being run.
   * @return The EventScore.
   * @throws EventException If problems occur while processing this event.
   */
  public EventScore computeScore(Match match, Team team, XMLGregorianCalendar timestamp)
      throws EventException {
    // [0] create the default score, which awards no points.
    EventScore score = new EventScore(timestamp, match, team);
    // [1] find the project and the sensorbase client.
    Project project = match.getProject(team);
    SensorBaseClient client = match.getSensorBaseClient(team);
    // [2] get the Build sensor data for the previous interval of time.
    String projectName = project.getName();
    String owner = project.getOwner();
    String sdt = "Build";
    XMLGregorianCalendar endTime = timestamp;
    XMLGregorianCalendar startTime = Tstamp.incrementMinutes(endTime, wakeupInterval * -1);
    SensorDataIndex index;
    HackystatLogger.getLogger("org.hackystat.devcathlon", "devcathlon").info(
        String.format("Getting build sensor data for match %s and team %s", match.getName(), team
            .getName()));
    try {
      index = client.getProjectSensorData(owner, projectName, startTime, endTime, sdt);
    }
    catch (Exception e) {
      throw new EventException("Error retrieving build data.", e);
    }
    // [3] see if there's been a build.
    XMLGregorianCalendar ciBuildTime = getCiBuildTime(client, index);

    // [4] if there has not been a build, return the default score right now.
    if (ciBuildTime == null) {
      score.setExplanation("No recent build, so no score.");
      return score;
    }

    // [5] there has been a build, so see if there is coverage since then.
    DailyProjectDataClient dpdClient = match.getDailyProjectDataClient(team);
    HackystatLogger.getLogger("org.hackystat.devcathlon", "devcathlon").info(
        String.format("Getting DPD coverage data for match %s and team %s", match.getName(), team
            .getName()));
    CoverageDailyProjectData coverageDpd;
    try {
      coverageDpd = dpdClient.getCoverage(owner, projectName, ciBuildTime, typeOfCoverage);
    }
    catch (Exception e) {
      throw new EventException(String.format("Error getting coverage for match %s and team %s",
          match.getName(), team.getName()), e);
    }

    // [6] if no coverage since then, return the default score.
    if (!coverageDpd.hasCoverageData()) {
      score.setExplanation("Recent build, but no coverage since then, so no score.");
      return score;
    }

    // [7] there is coverage data since the last CI build, so compute the score.
    int currCoverage = coverageDpd.getPercentageCoverage();
    HackystatLogger.getLogger("org.hackystat.devcathlon", "devcathlon").info(
        typeOfCoverage + " coverage is: " + currCoverage);
    score.setBaseScore(computeBaseScore(score, currCoverage));
    score.addBonusScore(firstTimeBonusName, computeBonusScore(score, currCoverage));

    // [8] now that we've computed the score, store coverage for the next time we run this event.
    this.lastCoverage = currCoverage;
    this.hasLastCoverage = true;
    // Finally return the final score.
    return score;
  }

  /**
   * Computes the base score given the current coverage. If there is no prior coverage, return 0. If
   * the current coverage is over the UpperThreshold, return 0. Otherwise return between -10 and +10
   * depending upon change in coverage.
   * 
   * @param score The EventScore.
   * @param currCoverage The current coverage percentage.
   * @return The base score.
   */
  private int computeBaseScore(EventScore score, int currCoverage) {
    if (!this.hasLastCoverage) {
      score.setExplanation("No prior coverage. Base score is 0.");
      return 0;
    }
    if (currCoverage > upperThreshold) {
      score.setExplanation("Current coverage is above threshold. Base score is 0.");
      return 0;
    }

    int diff = currCoverage - this.lastCoverage;
    if (diff < -10) {
      score.setExplanation("Coverage has dropped more than 10%. Base score is -10.");
      return -10;
    }
    else if (diff > 10) {
      score.setExplanation("Coverage has increased more than 10%. Base score is +10.");
      return 10;
    }
    else {
      score.setExplanation("Base score reflects coverage change of: " + diff + "%.");
      return diff;
    }
  }

  /**
   * Computes the bonus score given the current coverage. Return 0 if the bonus score has already
   * been given out or if the current coverage is below the upper threshold. Otherwise, return 1 and
   * set the bonus as having already been provided.
   * 
   * @param score The EventScore.
   * @param currCoverage The current coverage.
   * @return The bonus score.
   */
  private int computeBonusScore(EventScore score, int currCoverage) {
    if (this.hasFirstTimeBonus) {
      score.appendExplanation("Bonus already awarded; no bonus score.");
      return 0;
    }

    if (currCoverage < upperThreshold) {
      score.appendExplanation("Current coverage below threshold; no bonus score.");
      return 0;
    }
    else {
      this.hasFirstTimeBonus = true;
      score.appendExplanation("Coverage above threshold for first time! Bonus score awarded!");
      return 1;
    }
  }

  /**
   * Returns the timestamp of the first CI Build sensor data instance, or null if none was found. A
   * CI Build sensor data instance has a "Type" property whose value is "continuous.integration".
   * 
   * @param client The sensorbase client.
   * @param index The SensorDataIndex.
   * @return A timestamp of the first CI Build instance, or null if none was found.
   */
  private XMLGregorianCalendar getCiBuildTime(SensorBaseClient client, SensorDataIndex index) {
    try {
      for (SensorDataRef ref : index.getSensorDataRef()) {
        SensorData data = client.getSensorData(ref);
        Property buildTypeProperty = data.findProperty("Type");
        if ((buildTypeProperty != null)
            && (buildTypeProperty.getValue().equals("continuous.integration"))) {
          return data.getTimestamp();
        }
      }
      return null;
    }
    catch (Exception e) {
      return null;
    }
  }
}
